﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using Qreed.Collections;

namespace Qreed.Drawing.Drawing2D
{
    public class LineSegment2D
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="LineSegment2D"/> class.
        /// </summary>
        /// <param name="copyFrom">The copy from.</param>
        /// <param name="startIndex">The start index.</param>
        /// <param name="length">The length.</param>
        public LineSegment2D(PointF[] copyFrom, int startIndex, int length) : this()
        {
            // GraphicsPath sometimes starts and ends with the same point, which
            // gives some problems when triangulating

            if (copyFrom[startIndex] == copyFrom[startIndex + length - 1])
                length -= 1;

            for (int i = 0; i < length; i++)
            {
                if (i > 0 && copyFrom[startIndex - 1] == copyFrom[startIndex])
                {
                    // Don't add the same point twice.
                    ++startIndex; continue;
                }

                Vertices.Add(copyFrom[startIndex++]);
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ClosedLineSegment2D"/> class.
        /// </summary>
        /// <param name="vertices">The vertices.</param>
        public LineSegment2D(PointF[] vertices) : this(vertices, 0, vertices.Length)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="LineSegment2D"/> class.
        /// </summary>
        public LineSegment2D()
        {
            Vertices.OnChange += new EventHandler<NotifyOnChangeEventArgs<PointF>>(VerticesOnChange);
        }

        void VerticesOnChange(object sender, NotifyOnChangeEventArgs<PointF> e)
        {
            _segmentType = SegmentType.Unknown;
            _windingOrder = WindingOrder.Unknown;
        }

        private NotifyOnChangeCollection<PointF> _vertices = new NotifyOnChangeCollection<PointF>();
        /// <summary>
        /// Gets the vertices.
        /// </summary>
        /// <value>The vertices.</value>
        public NotifyOnChangeCollection<PointF> Vertices
        {
            get { return this._vertices; }
        }
			
        /// <summary>
        /// Gets the <see cref="System.Drawing.PointF"/> at the specified index.
        /// </summary>
        /// <value>The point at the given index.</value>
        public PointF this[int index]
        {
            get
            {
                while(index < 0)
                    index += Count;

                if (index >= Count)
                    index %= Count;
                
                return Vertices[index];
            }
        }

        /// <summary>
        /// Gets the count.
        /// </summary>
        /// <value>The count.</value>
        public int Count
        {
            get { return Vertices.Count; }
        }

        private WindingOrder _windingOrder = WindingOrder.Unknown;
        /// <summary>
        /// Gets or sets the winding order.
        /// </summary>
        /// <value>The winding order.</value>
        /// <remarks>Note that in windows the upper left corner is y = 0, which flips the actual visual order.</remarks>
        public WindingOrder WindingOrder
        {
            get
            {
                if (_windingOrder == WindingOrder.Unknown)
                    CalculateWindingOrder();

                return this._windingOrder;
            }
            set
            {
                if (value == WindingOrder.Unknown)
                    throw (new Exception("Can't set winding order to unknown."));

                if (WindingOrder != value) // Requesting the property forces calculation of winding if needed
                {
                    NotifyOnChangeCollection<PointF> vertices = new NotifyOnChangeCollection<PointF>();
                   
                    while (Vertices.Count > 0)
                    {
                        vertices.Add(Vertices[Vertices.Count - 1]);
                        Vertices.RemoveAt(Vertices.Count - 1);
                    }
                   
                    vertices.OnChange += new EventHandler<NotifyOnChangeEventArgs<PointF>>(this.VerticesOnChange);
                    _vertices = vertices;
                }

                this._windingOrder = value;
            }
        }

        private SegmentType _segmentType = SegmentType.Unknown;
        /// <summary>
        /// Gets the type of the segment.
        /// </summary>
        /// <value>The type of the segment.</value>
        public SegmentType SegmentType
        {
            get
            {
                if (_segmentType == SegmentType.Unknown)
                    CalculateSegmentType();

                return this._segmentType;
            }
        }

        /// <summary>
        /// Gets the bounds.
        /// </summary>
        /// <returns>A rectangle containing the bounds.</returns>
        public RectangleF GetBounds()
        {
            if (Count == 0)
                return RectangleF.Empty;

            PointF min;
            PointF max;

            GetMinMaxPoints(out min, out max);

            return new RectangleF(min, new SizeF(max.X - min.X, max.Y - min.Y));
        }

        /// <summary>
        /// Gets the min max points.
        /// </summary>
        /// <param name="min">The min.</param>
        /// <param name="max">The max.</param>
        public void GetMinMaxPoints(out PointF min, out PointF max)
        {
            if (Count == 0)
                min = max = PointF.Empty;

            min = new PointF(Vertices[0].X, Vertices[0].Y);
            max = new PointF(Vertices[0].X, Vertices[0].Y);

            for (int i = 1; i < Count; i++)
            {
                PointF pt = Vertices[i];

                min.X = Math.Min(min.X, pt.X);
                min.Y = Math.Min(min.Y, pt.Y);
                max.X = Math.Max(max.X, pt.X);
                max.Y = Math.Max(max.Y, pt.Y);
            }
        }

        /// <summary>
        /// Locates the intersection point.
        /// </summary>
        /// <param name="line">The line.</param>
        /// <param name="includeStartEndPoint">if set to <c>true</c> the method will include start and end points.</param>
        /// <returns>The point of intersection or <c>null</c> if the line doesn't intersects.</returns>
        public PointF? GetIntersectionPoint(Line2D line, bool includeStartEndPoint)
        {
            for (int i = 0; i < Count; i++)
            {
                Line2D edge = new Line2D(this[i], this[i + 1]);

                PointF? point = line.GetIntersectionPoint(edge, includeStartEndPoint);

                if (point.HasValue)
                    return point;
            }

            return null;
        }
        
        /// <summary>
        /// Calculates the type of the segment.
        /// </summary>
        private void CalculateSegmentType()
        {
            int c = 0;

            for (int i = 0; i < Count; i++)
            {
                float sign = GetCrossProduct(i);

                if (sign < 0.0f)
                    c |= 1;
                else if (sign > 0.0f)
                    c |= 2;

                if (c == 3)
                {
                    _segmentType = SegmentType.Concave;
                    return;
                }
            }

            _segmentType = SegmentType.Convex;
        }

        /// <summary>
        /// Gets the cross product.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <returns>The cross prduct</returns>
        protected float GetCrossProduct(int index)
        {
            PointF p0 = this[index - 1];
            PointF p1 = this[index];
            PointF p2 = this[index + 1];

            PointF v0 = new PointF(p1.X - p0.X, p1.Y - p0.Y);
            PointF v1 = new PointF(p2.X - p1.X, p2.Y - p1.Y);

            float sign = (v0.X * v1.Y) - (v0.Y * v1.X);

            return sign;
        }

        /// <summary>
        /// Calculates the winding order.
        /// </summary>
        private void CalculateWindingOrder()
        {
            float area = GetAreaSigned();

            if (area < 0.0f)
                _windingOrder = WindingOrder.ClockWise;
            else
                _windingOrder = WindingOrder.CounterClockWise;

            // Debug.WriteLine("Area: " + area);
        }

        /// <summary>
        /// Gets the area signed.
        /// </summary>
        /// <returns></returns>
        public float GetAreaSigned()
        {
            float area = 0.0f;

            if (Count >= 3)
            {
                for (int i = 1; i < Count; i++)
                {
                    float x1 = this[i].X - this[0].X;
                    float y1 = this[i].Y - this[0].Y;

                    float x2 = this[i + 1].X - this[0].X;
                    float y2 = this[i + 1].Y - this[0].Y;

                    float cross = x1 * y2 - x2 * y1;

                    area += cross;
                }
            }

            return area * 0.5f;
        }
    }
}
